﻿/* Copyright (C) 2012 by John Cronin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace davserv
{
    class Acl
    {
        bool write;
        bool read;
        bool create;
        bool delete;
        bool addproperty;
        bool deleteproperty;
        bool changeproperty;

        uri _uri;

        public bool Write { get { return write; } }
        public bool Read { get { return read; } }
        public bool Create { get { return create; } }
        public bool Delete { get { return delete; } }
        public bool AddProperty { get { return addproperty; } }
        public bool DeleteProperty { get { return deleteproperty; } }
        public bool ChangeProperty { get { return changeproperty; } }

        public uri Uri { get { return _uri; } }

        public static Acl GetAcl(uri uri)
        {
            Acl ret = new Acl();
            ret._uri = uri;

            /* For now just give everyone access to everything */
            ret.write = true;
            ret.read = true;
            ret.create = true;
            ret.delete = true;
            ret.addproperty = true;
            ret.deleteproperty = true;
            ret.changeproperty = true;

            return ret;
        }

        public enum PermissionType { Read, Write };
        public class Permission { public bool Allowed; public bool AuthRequested; }

        internal static Permission CheckPermissions(uri uri, BackendManager bm, HttpServer.Request r, PermissionType permissionType)
        {
            // See if we can perform the requested operation on the requested uri

            // This is defined if authorization as a user may help
            bool auth_may_help = false;

            // First see if there is any user authorized in the request
            string auth_user = null;
            string[] auth_groups = null;

            if (r.Headers.ContainsKey("Authorization"))
            {
                Auth.AuthHeader aheader = Auth.DecodeAuthorizationHeader(r.Headers["Authorization"]);

                if (aheader.AuthType == "Digest")
                {
                    Auth.IAuthProvider aprovider = Auth.GetAuthProvider(bm.GetConfigurationObject("auth_source", uri).ConfigurationObject);
                    aprovider.Init(bm.GetConfigurationObject("auth_source_options", uri).ConfigurationObject);

                    string user = aheader.Headers["username"];
                    string realm = aheader.Headers["realm"];

                    string ha1 = aprovider.GetHA1Hash(user, realm);
                    if (ha1 != null)
                    {
                        string a2 = r.Method + ":" + aheader.Headers["uri"];
                        string ha2 = Auth.ComputeMD5Hash(a2);

                        string response = "";
                        if (aheader.Headers.ContainsKey("qop") && ((aheader.Headers["qop"] == "auth") || (aheader.Headers["qop"] == "auth-int")))
                        {
                            response = ha1 + ":" + aheader.Headers["nonce"] + ":" + aheader.Headers["nc"] + ":" + aheader.Headers["cnonce"] + ":" + aheader.Headers["qop"] + ":" + ha2;
                            response = Auth.ComputeMD5Hash(response);
                        }
                        else
                        {
                            response = ha1 + ":" + aheader.Headers["nonce"] + ":" + ha2;
                            response = Auth.ComputeMD5Hash(response);
                        }

                        bool authorized = Auth.IgnoreCaseCompare(response, aheader.Headers["response"]);
                        if (authorized)
                        {
                            auth_user = user;
                            auth_groups = aprovider.GetGroups(user, realm);
                        }
                    }
                }
            }

            // Get the names of the properties involved in authorization and read them
            string order_prop = "read_order";
            string allow_prop = "read_allow";
            string deny_prop = "read_deny";

            if (permissionType == PermissionType.Write)
            {
                order_prop = "write_order";
                allow_prop = "write_allow";
                deny_prop = "write_deny";
            }

            string order = bm.GetConfigurationObject(order_prop, uri).ConfigurationObject;
            string allow = bm.GetConfigurationObject(allow_prop, uri).ConfigurationObject;
            string deny = bm.GetConfigurationObject(deny_prop, uri).ConfigurationObject;

            if (order == null)
                order = "allow, deny";
            if (allow == null)
                allow = "none";
            if (deny == null)
                deny = "all";

            // Now work through the order, accepting or refusing as we go
            string[] order_list = order.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (string order_list_entry in order_list)
            {
                string test_list = allow;
                if (order_list_entry == "deny")
                    test_list = deny;

                // see if we match any of the entries in 'test_list'
                string[] tests = test_list.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                bool match = false;

                foreach (string test in tests)
                {
                    if (test == "all")
                    {
                        match = true;
                        break;
                    }

                    if (test == "none")
                    {
                        match = false;
                        break;
                    }

                    if (test.StartsWith("+"))
                    {
                        if ((auth_user != null) && (test.Substring(1) == auth_user))
                        {
                            match = true;
                            break;
                        }
                        auth_may_help = true;
                    }
                    else if (test.StartsWith("@"))
                    {
                        if (auth_groups != null)
                        {
                            foreach (string auth_group in auth_groups)
                            {
                                if (test.Substring(1) == auth_group)
                                {
                                    match = true;
                                    break;
                                }
                            }
                            if (match)
                                break;
                        }
                           
                        auth_may_help = true;
                    }
                    else if ((r.Client.Client.RemoteEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) ||
                        (r.Client.Client.RemoteEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6))
                    {
                        System.Net.IPEndPoint ep = r.Client.Client.RemoteEndPoint as System.Net.IPEndPoint;

                        // check if the ip address is matched
                        if (ep.Address.ToString() == test)
                        {
                            match = true;
                            break;
                        }

                        // check if its in the form ip address/subnet mask
                        if (test.Contains('/'))
                        {
                            byte[] client_b = ep.Address.GetAddressBytes();

                            string[] test_split = test.Split('/');

                            string ip = test_split[0];
                            string mask = test_split[1];

                            byte[] test_b = new byte[0];
                            byte[] mask_b = new byte[0];

                            /* we have to implement GetAddressBytes() manually for test and mask
                             * as we use things like 192.168.0.0/255.255.255.0 in the test,
                             * and .NET throws an exception when we do IPAddress.Parse("192.168.0.0")
                             * as it is an invalid IP addres (but is still a valid test)
                             * 
                             * This only works for IPv4 currently
                             */

                            string[] test_parts = ip.Split('.');
                            if (test_parts.Length == 4)
                            {
                                test_b = new byte[4];
                                for (int i = 0; i < 4; i++)
                                {
                                    int c = int.Parse(test_parts[i]);
                                    test_b[i] = (byte)c;
                                }
                            }

                            string[] mask_parts = mask.Split('.');
                            if (mask_parts.Length == 4)
                            {
                                mask_b = new byte[4];
                                for (int i = 0; i < 4; i++)
                                {
                                    int c = int.Parse(mask_parts[i]);
                                    mask_b[i] = (byte)c;
                                }
                            }
                            else
                            {
                                int cidr;
                                if (int.TryParse(mask, out cidr))
                                {
                                    // CIDR format - the most significant 'cidr' bits are set to 1 in the mask, the others to 0
                                    mask_b = new byte[test_b.Length];

                                    for (int i = 0; i < mask_b.Length; i++)
                                    {
                                        if (((i + 1) * 8) <= cidr)
                                            mask_b[i] = 0xff;
                                        else if ((i * 8) >= cidr)
                                            mask_b[i] = 0x00;
                                        else
                                        {
                                            // cidr bit falls somewhere within this group
                                            // upper 'bit_no' bits are 1s
                                            int bit_no = cidr - (i * 8);

                                            // lower shift_count bits are 0s
                                            int shift_count = 8 - bit_no;

                                            byte v = (byte)((0xff << shift_count) & 0xff);
                                            mask_b[i] = v;
                                        }


                                    }
                                }

                            }

                            // now perform the test ((client & mask) == (test & mask))
                            if((client_b.Length == mask_b.Length) &&
                                ((test_b.Length == mask_b.Length)))
                            {
                                bool ip_match = true;

                                for(int i = 0; i < client_b.Length; i++)
                                {
                                    if((client_b[i] & mask_b[i]) != (test_b[i] & mask_b[i]))
                                    {
                                        ip_match = false;
                                        break;
                                    }
                                }

                                if(ip_match)
                                {
                                    match = true;
                                    break;
                                }
                            }
                        }
                            // match anything that is not an ip address
                        else if (System.Text.RegularExpressions.Regex.Match(test, "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b").Success == false)
                        {
                            // do a reverse DNS lookup to check whether its an allowed domain
                            if (!r.ClientHostnameWait.IsCompleted)
                                r.ClientHostnameWait.AsyncWaitHandle.WaitOne();

                            if (r.ClientHostname == null)
                            {
                                try
                                {
                                    System.Net.IPHostEntry he = System.Net.Dns.EndGetHostEntry(r.ClientHostnameWait);
                                    r.ClientHostname = he.HostName;
                                }
                                catch (Exception)
                                {
                                    r.ClientHostname = "unknown";
                                }
                            }

                            if ((r.ClientHostname == test) || (r.ClientHostname.EndsWith("." + test)))
                            {
                                match = true;
                                break;
                            }
                        }
                    }
                }


                /* Now all the tests are done, see if we've matched anything, and interpret that
                 * according to the allow or deny attribute
                 */

                if (match)
                {
                    if (order_list_entry == "deny")
                        return new Permission { Allowed = false, AuthRequested = auth_may_help };
                    else if (order_list_entry == "allow")
                        return new Permission { Allowed = true, AuthRequested = auth_may_help };
                }
            }

            return new Permission { Allowed = false, AuthRequested = auth_may_help };
        }
    }
}
